package 更新类

import (
	"archive/zip"
	"bytes"
	"context"
	"errors"
	数组类 "gitee.com/go_888/extends/goframe/garray"
	文件类 "gitee.com/go_888/extends/goframe/gfile"
	文本类 "gitee.com/go_888/extends/goframe/gstr"
	网页类 "gitee.com/go_888/hertz/hertz/pkg/app/client"
	"gitee.com/go_888/hertz/hertz/pkg/app/client/retry"
	"gitee.com/go_888/hertz/hertz/pkg/protocol"
	"gitee.com/go_888/update/internal/go-update"
	"gitee.com/go_888/update/internal/osext"
	"github.com/blang/semver"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"syscall"
)

// X更新单文件 执行文件替换操作，以更新应用程序。
// 该函数接收一个可变参数 替换路径，用于指定更新文件的路径。
// 替换路劲:空字符串表示“正在运行的程序的可执行文件”。
// 函数返回错误，如果文件读取失败、文件为空、权限检查失败或更新应用失败。
func X更新单文件(字节集 []byte, 替换路径 ...string) error {
	if len(字节集) == 0 {
		return errors.New("执行替换失败,字节集长度为0")
	}

	// 初始化更新配置。
	更新配置 := update.Options{}
	// 如果提供了替换路径，设置更新配置的目标路径。
	if len(替换路径) > 0 {
		更新配置.TargetPath = 替换路径[0]
		if 文件类.X是否存在文件(更新配置.TargetPath) == false {
			return errors.New("执行替换失败,需要替换的文件不存在:" + 更新配置.TargetPath)
		}
	}

	// 更新权限预判断.
	err := 更新配置.CheckPermissions()
	if err != nil {
		// 如果权限检查失败，返回错误。
		return err
	}

	// 应用更新。
	err = update.Apply(bytes.NewBuffer(字节集), 更新配置)
	if err != nil {
		//更新失败,回滚
		if rerr := update.RollbackError(err); rerr != nil {
			return rerr
		}
	}
	return err
}

// X重启进程 函数用于重新启动当前应用程序, exe启动参数会从 os.Args中获取, 并传递给新进程。
// 它接受一个可变参数 重启路径，用于指定重新启动程序的路径, 如果没有提供路径，它将尝试使用当前执行文件的路径。
// 返回错误: 如果重新启动过程中的任何步骤失败。
//
// 此方法来自: https://github.com/minio/selfupdate 的 restart()函数
func X重启进程(启动路径 ...string) error {
	// 初始化可执行路径为空字符串。
	可执行路径 := ""

	// 如果提供了重启路径，使用第一个路径作为可执行路径。
	if len(启动路径) > 0 {
		可执行路径 = 启动路径[0]
	}

	// 获取当前工作目录。
	wd, err := os.Getwd()
	if err != nil {
		return err
	}

	// 如果可执行路径为空，尝试获取当前执行文件的路径。
	if 可执行路径 == "" {
		可执行路径, err = osext.Executable()
		if err != nil {
			return err
		}
	}

	// 使用获取到的可执行路径和当前进程的参数、环境变量等信息启动新的进程。
	// 新进程将在当前工作目录中启动，并继承当前进程的标准输入、输出和错误流。
	_, err = os.StartProcess(可执行路径, os.Args, &os.ProcAttr{
		Dir:   wd,
		Env:   os.Environ(),
		Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
		Sys:   &syscall.SysProcAttr{},
	})

	// 如果新进程启动成功，退出当前进程。
	if err == nil {
		os.Exit(0)
	}

	// 返回可能的错误。
	return err
}

// 解压zip字节集 函数将一个zip格式的字节集解压到指定的保存路径。
// 此方法与gf的zip.Unzip函数类似，但此方法可以处理GBK编码的中文文件名。
// zip字节集: 待解压的zip格式字节集。
// 保存路径: 解压后文件的保存路径。
// 返回值: 如果解压过程中发生错误，则返回错误。
func X解压zip字节集(zip字节集 []byte, 保存路径 string) error {
	zipReader, err := zip.NewReader(bytes.NewReader(zip字节集), int64(len(zip字节集)))
	if err != nil {
		return err
	}

	var decodeName string
	for _, f := range zipReader.File {
		if f.Flags == 0 {
			i := bytes.NewReader([]byte(f.Name))
			decoder := transform.NewReader(i, simplifiedchinese.GB18030.NewDecoder())
			content, _ := ioutil.ReadAll(decoder)
			decodeName = string(content)
		} else {
			decodeName = f.Name
		}

		fpath := filepath.Join(保存路径, decodeName)
		if f.FileInfo().IsDir() {
			os.MkdirAll(fpath, os.ModePerm)
		} else {
			if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
				return err
			}

			inFile, err := f.Open()
			if err != nil {
				return err
			}
			defer inFile.Close()

			outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
			if err != nil {
				return err
			}
			defer outFile.Close()

			_, err = io.Copy(outFile, inFile)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// X取最新版本号 接收一个版本号数组，返回数组内的最新版本号字符串。
// 使用 semver 库来处理版本号，以确保版本号的有效性和正确排序。
// 参数:
// 版本号数组 []string: 一个包含版本号的字符串数组。
// 返回值:
// string: 数组内的最新版本号字符串，如果没有有效版本号，则返回空字符串。
func X取最新版本号(版本号数组 []string) string {
	// "github.com/blang/semver"
	// 这个库的好处是,使用了值而不是值引用,如果版本号不合格,会直接设置成"0.0.0", 避免panic
	var semVerList []semver.Version
	for _, v := range 版本号数组 {
		version, _ := semver.Make(v)
		semVerList = append(semVerList, version)
	}
	semver.Sort(semVerList) // 排序
	// 最新
	if len(semVerList) > 0 {
		return semVerList[len(semVerList)-1].String()
	}
	return ""
}

// X版本号排序 接受一个版本号字符串数组，并返回按语义化版本号排序后的数组。
// 该函数使用 github.com/blang/semver 库进行版本号的解析和排序。
// 参数:
// 版本号数组 []string: 一个包含版本号字符串的数组。
// 返回值:
// []string: 按语义化版本号排序后的版本号字符串数组。
func X版本号排序(版本号数组 []string) []string {
	// "github.com/blang/semver"
	// 这个库的好处是,使用了值而不是值引用,如果版本号不合格,会直接设置成"0.0.0", 避免panic
	var semVerList []semver.Version
	for _, v := range 版本号数组 {
		version, _ := semver.Make(v)
		semVerList = append(semVerList, version)
	}
	semver.Sort(semVerList) // 排序

	// 整理返回数组
	返回版本号数组 := 数组类.X创建文本()
	for _, v := range semVerList {
		返回版本号数组.X加入前面(v.String())
	}
	return 返回版本号数组.X取数组()
}

func 发送get(url string) (状态码 int, body string) {
	client, _ := 网页类.X创建(
		网页类.X配置请求重试策略(
			retry.X设置最大重试次数(5),
		),
	)
	client.X设置重试条件(func(req *protocol.Request, resp *protocol.Response, err error) bool {
		return resp.X取状态码() != 200 || err != nil
	})

	req := protocol.X取空请求对象()
	resp := protocol.X取空响应对象()

	req.X设置请求方法("GET")
	req.X设置请求URI(url)

	//req.X协议头.Set("Content-Type", "application/json")
	err := client.X执行(context.Background(), req, resp)
	if err != nil {
		return 0, ""
	}

	return resp.X取状态码(), string(resp.X取主体数据())
}

func X判断链接是否为下载链接(url string) (bool, error) {
	client, _ := 网页类.X创建(
		网页类.X配置请求重试策略(
			retry.X设置最大重试次数(5),
		),
	)
	client.X设置重试条件(func(req *protocol.Request, resp *protocol.Response, err error) bool {
		return resp.X取状态码() != 200 || err != nil
	})

	req := protocol.X取空请求对象()
	resp := protocol.X取空响应对象()

	req.X设置请求方法("HEAD")
	req.X设置请求URI(url)
	err := client.X执行(context.Background(), req, resp)
	if err != nil {
		return false, err
	}

	if resp.X取状态码() != 200 {
		return false, errors.New("资源出错,请确定链接是否有误.")
	}

	//判断是否是下载的链接
	Content_Disposition := resp.X协议头.X取值("Content-Disposition")
	类型数组 := []string{"attachment", "inline", "filename"}
	for _, v := range 类型数组 {
		if 文本类.X是否包含(Content_Disposition, v) {
			return true, nil
		}
	}

	//跳过html与json/xml,部分出错链接可能返回json/xml或html
	Content_Type := resp.X协议头.X取值("Content-Type")
	类型数组 = []string{"/html", "/json", "/xml"}
	for _, v := range 类型数组 {
		if 文本类.X是否包含(Content_Type, v) {
			return false, errors.New("资源出错,请确定链接是否有误.")
		}
	}
	//如果返回状态为200,且没有返回Content-Disposition,则认为该链接可以下载,但可能不是下载链接, 比如jpg文件.
	return true, nil
}
